package github

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	ghErrors "github.com/github/github-mcp-server/pkg/errors"
	"github.com/github/github-mcp-server/pkg/raw"
	"github.com/github/github-mcp-server/pkg/translations"
	"github.com/github/github-mcp-server/pkg/utils"
	"github.com/google/go-github/v79/github"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "get_commit",
		Description: t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_GET_COMMITS_USER_TITLE", "Get commit details"),
			ReadOnlyHint: true,
		},
		InputSchema: WithPagination(&jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"sha": {
					Type:        "string",
					Description: "Commit SHA, branch name, or tag name",
				},
				"include_diff": {
					Type:        "boolean",
					Description: "Whether to include file diffs and stats in the response. Default is true.",
					Default:     json.RawMessage(`true`),
				},
			},
			Required: []string{"owner", "repo", "sha"},
		}),
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		sha, err := RequiredParam[string](args, "sha")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		includeDiff, err := OptionalBoolParamWithDefault(args, "include_diff", true)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		pagination, err := OptionalPaginationParams(args)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		opts := &github.ListOptions{
			Page:    pagination.Page,
			PerPage: pagination.PerPage,
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}
		commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				fmt.Sprintf("failed to get commit: %s", sha),
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != 200 {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil, nil
		}

		// Convert to minimal commit
		minimalCommit := convertToMinimalCommit(commit, includeDiff)

		r, err := json.Marshal(minimalCommit)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// ListCommits creates a tool to get commits of a branch in a repository.
func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "list_commits",
		Description: t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100)."),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"),
			ReadOnlyHint: true,
		},
		InputSchema: WithPagination(&jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"sha": {
					Type:        "string",
					Description: "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.",
				},
				"author": {
					Type:        "string",
					Description: "Author username or email address to filter commits by",
				},
			},
			Required: []string{"owner", "repo"},
		}),
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		sha, err := OptionalParam[string](args, "sha")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		author, err := OptionalParam[string](args, "author")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		pagination, err := OptionalPaginationParams(args)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		// Set default perPage to 30 if not provided
		perPage := pagination.PerPage
		if perPage == 0 {
			perPage = 30
		}
		opts := &github.CommitsListOptions{
			SHA:    sha,
			Author: author,
			ListOptions: github.ListOptions{
				Page:    pagination.Page,
				PerPage: perPage,
			},
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}
		commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				fmt.Sprintf("failed to list commits: %s", sha),
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != 200 {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to list commits: %s", string(body))), nil, nil
		}

		// Convert to minimal commits
		minimalCommits := make([]MinimalCommit, len(commits))
		for i, commit := range commits {
			minimalCommits[i] = convertToMinimalCommit(commit, false)
		}

		r, err := json.Marshal(minimalCommits)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// ListBranches creates a tool to list branches in a GitHub repository.
func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "list_branches",
		Description: t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_LIST_BRANCHES_USER_TITLE", "List branches"),
			ReadOnlyHint: true,
		},
		InputSchema: WithPagination(&jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
			},
			Required: []string{"owner", "repo"},
		}),
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		pagination, err := OptionalPaginationParams(args)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		opts := &github.BranchListOptions{
			ListOptions: github.ListOptions{
				Page:    pagination.Page,
				PerPage: pagination.PerPage,
			},
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to list branches",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to list branches: %s", string(body))), nil, nil
		}

		// Convert to minimal branches
		minimalBranches := make([]MinimalBranch, 0, len(branches))
		for _, branch := range branches {
			minimalBranches = append(minimalBranches, convertToMinimalBranch(branch))
		}

		r, err := json.Marshal(minimalBranches)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository.
func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "create_or_update_file",
		Description: t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations."),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_CREATE_OR_UPDATE_FILE_USER_TITLE", "Create or update file"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner (username or organization)",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"path": {
					Type:        "string",
					Description: "Path where to create/update the file",
				},
				"content": {
					Type:        "string",
					Description: "Content of the file",
				},
				"message": {
					Type:        "string",
					Description: "Commit message",
				},
				"branch": {
					Type:        "string",
					Description: "Branch to create/update the file in",
				},
				"sha": {
					Type:        "string",
					Description: "Required if updating an existing file. The blob SHA of the file being replaced.",
				},
			},
			Required: []string{"owner", "repo", "path", "content", "message", "branch"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		path, err := RequiredParam[string](args, "path")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		content, err := RequiredParam[string](args, "content")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		message, err := RequiredParam[string](args, "message")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		branch, err := RequiredParam[string](args, "branch")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		// json.Marshal encodes byte arrays with base64, which is required for the API.
		contentBytes := []byte(content)

		// Create the file options
		opts := &github.RepositoryContentFileOptions{
			Message: github.Ptr(message),
			Content: contentBytes,
			Branch:  github.Ptr(branch),
		}

		// If SHA is provided, set it (for updates)
		sha, err := OptionalParam[string](args, "sha")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		if sha != "" {
			opts.SHA = github.Ptr(sha)
		}

		// Create or update the file
		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}
		fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create/update file",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != 200 && resp.StatusCode != 201 {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to create/update file: %s", string(body))), nil, nil
		}

		r, err := json.Marshal(fileContent)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// CreateRepository creates a tool to create a new GitHub repository.
func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "create_repository",
		Description: t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account or specified organization"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_CREATE_REPOSITORY_USER_TITLE", "Create repository"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"name": {
					Type:        "string",
					Description: "Repository name",
				},
				"description": {
					Type:        "string",
					Description: "Repository description",
				},
				"organization": {
					Type:        "string",
					Description: "Organization to create the repository in (omit to create in your personal account)",
				},
				"private": {
					Type:        "boolean",
					Description: "Whether repo should be private",
				},
				"autoInit": {
					Type:        "boolean",
					Description: "Initialize with README",
				},
			},
			Required: []string{"name"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		name, err := RequiredParam[string](args, "name")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		description, err := OptionalParam[string](args, "description")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		organization, err := OptionalParam[string](args, "organization")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		private, err := OptionalParam[bool](args, "private")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		autoInit, err := OptionalParam[bool](args, "autoInit")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		repo := &github.Repository{
			Name:        github.Ptr(name),
			Description: github.Ptr(description),
			Private:     github.Ptr(private),
			AutoInit:    github.Ptr(autoInit),
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}
		createdRepo, resp, err := client.Repositories.Create(ctx, organization, repo)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create repository",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusCreated {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to create repository: %s", string(body))), nil, nil
		}

		// Return minimal response with just essential information
		minimalResponse := MinimalResponse{
			ID:  fmt.Sprintf("%d", createdRepo.GetID()),
			URL: createdRepo.GetHTMLURL(),
		}

		r, err := json.Marshal(minimalResponse)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "get_file_contents",
		Description: t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"),
			ReadOnlyHint: true,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner (username or organization)",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"path": {
					Type:        "string",
					Description: "Path to file/directory (directories must end with a slash '/')",
					Default:     json.RawMessage(`"/"`),
				},
				"ref": {
					Type:        "string",
					Description: "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`",
				},
				"sha": {
					Type:        "string",
					Description: "Accepts optional commit SHA. If specified, it will be used instead of ref",
				},
			},
			Required: []string{"owner", "repo"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		path, err := RequiredParam[string](args, "path")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		ref, err := OptionalParam[string](args, "ref")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		sha, err := OptionalParam[string](args, "sha")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return utils.NewToolResultError("failed to get GitHub client"), nil, nil
		}

		rawOpts, err := resolveGitReference(ctx, client, owner, repo, ref, sha)
		if err != nil {
			return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil
		}

		// If the path is (most likely) not to be a directory, we will
		// first try to get the raw content from the GitHub raw content API.

		var rawAPIResponseCode int
		if path != "" && !strings.HasSuffix(path, "/") {
			// First, get file info from Contents API to retrieve SHA
			var fileSHA string
			opts := &github.RepositoryContentGetOptions{Ref: ref}
			fileContent, _, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
			if respContents != nil {
				defer func() { _ = respContents.Body.Close() }()
			}
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to get file SHA",
					respContents,
					err,
				), nil, nil
			}
			if fileContent == nil || fileContent.SHA == nil {
				return utils.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil, nil
			}
			fileSHA = *fileContent.SHA

			rawClient, err := getRawClient(ctx)
			if err != nil {
				return utils.NewToolResultError("failed to get GitHub raw content client"), nil, nil
			}
			resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
			if err != nil {
				return utils.NewToolResultError("failed to get raw repository content"), nil, nil
			}
			defer func() {
				_ = resp.Body.Close()
			}()

			if resp.StatusCode == http.StatusOK {
				// If the raw content is found, return it directly
				body, err := io.ReadAll(resp.Body)
				if err != nil {
					return utils.NewToolResultError("failed to read response body"), nil, nil
				}
				contentType := resp.Header.Get("Content-Type")

				var resourceURI string
				switch {
				case sha != "":
					resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path)
					if err != nil {
						return nil, nil, fmt.Errorf("failed to create resource URI: %w", err)
					}
				case ref != "":
					resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path)
					if err != nil {
						return nil, nil, fmt.Errorf("failed to create resource URI: %w", err)
					}
				default:
					resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
					if err != nil {
						return nil, nil, fmt.Errorf("failed to create resource URI: %w", err)
					}
				}

				// Determine if content is text or binary
				isTextContent := strings.HasPrefix(contentType, "text/") ||
					contentType == "application/json" ||
					contentType == "application/xml" ||
					strings.HasSuffix(contentType, "+json") ||
					strings.HasSuffix(contentType, "+xml")

				if isTextContent {
					result := &mcp.ResourceContents{
						URI:      resourceURI,
						Text:     string(body),
						MIMEType: contentType,
					}
					// Include SHA in the result metadata
					if fileSHA != "" {
						return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded text file (SHA: %s)", fileSHA), result), nil, nil
					}
					return utils.NewToolResultResource("successfully downloaded text file", result), nil, nil
				}

				result := &mcp.ResourceContents{
					URI:      resourceURI,
					Blob:     body,
					MIMEType: contentType,
				}
				// Include SHA in the result metadata
				if fileSHA != "" {
					return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)", fileSHA), result), nil, nil
				}
				return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil
			}
			rawAPIResponseCode = resp.StatusCode
		}

		if rawOpts.SHA != "" {
			ref = rawOpts.SHA
		}
		if strings.HasSuffix(path, "/") {
			opts := &github.RepositoryContentGetOptions{Ref: ref}
			_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
			if err == nil && resp.StatusCode == http.StatusOK {
				defer func() { _ = resp.Body.Close() }()
				r, err := json.Marshal(dirContent)
				if err != nil {
					return utils.NewToolResultError("failed to marshal response"), nil, nil
				}
				return utils.NewToolResultText(string(r)), nil, nil
			}
		}

		// The path does not point to a file or directory.
		// Instead let's try to find it in the Git Tree by matching the end of the path.

		// Step 1: Get Git Tree recursively
		tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get git tree",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		// Step 2: Filter tree for matching paths
		const maxMatchingFiles = 3
		matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
		if len(matchingFiles) > 0 {
			matchingFilesJSON, err := json.Marshal(matchingFiles)
			if err != nil {
				return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
			}
			resolvedRefs, err := json.Marshal(rawOpts)
			if err != nil {
				return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
			}
			return utils.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
		}

		return utils.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil, nil
	})

	return tool, handler
}

// ForkRepository creates a tool to fork a repository.
func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "fork_repository",
		Description: t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"organization": {
					Type:        "string",
					Description: "Organization to fork to",
				},
			},
			Required: []string{"owner", "repo"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		org, err := OptionalParam[string](args, "organization")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		opts := &github.RepositoryCreateForkOptions{}
		if org != "" {
			opts.Organization = org
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}
		forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts)
		if err != nil {
			// Check if it's an acceptedError. An acceptedError indicates that the update is in progress,
			// and it's not a real error.
			if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) {
				return utils.NewToolResultText("Fork is in progress"), nil, nil
			}
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to fork repository",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusAccepted {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil, nil
		}

		// Return minimal response with just essential information
		minimalResponse := MinimalResponse{
			ID:  fmt.Sprintf("%d", forkedRepo.GetID()),
			URL: forkedRepo.GetHTMLURL(),
		}

		r, err := json.Marshal(minimalResponse)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// DeleteFile creates a tool to delete a file in a GitHub repository.
// This tool uses a more roundabout way of deleting a file than just using the client.Repositories.DeleteFile.
// This is because REST file deletion endpoint (and client.Repositories.DeleteFile) don't add commit signing to the deletion commit,
// unlike how the endpoint backing the create_or_update_files tool does. This appears to be a quirk of the API.
// The approach implemented here gets automatic commit signing when used with either the github-actions user or as an app,
// both of which suit an LLM well.
func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "delete_file",
		Description: t("TOOL_DELETE_FILE_DESCRIPTION", "Delete a file from a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:           t("TOOL_DELETE_FILE_USER_TITLE", "Delete file"),
			ReadOnlyHint:    false,
			DestructiveHint: github.Ptr(true),
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner (username or organization)",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"path": {
					Type:        "string",
					Description: "Path to the file to delete",
				},
				"message": {
					Type:        "string",
					Description: "Commit message",
				},
				"branch": {
					Type:        "string",
					Description: "Branch to delete the file from",
				},
			},
			Required: []string{"owner", "repo", "path", "message", "branch"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		path, err := RequiredParam[string](args, "path")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		message, err := RequiredParam[string](args, "message")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		branch, err := RequiredParam[string](args, "branch")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		// Get the reference for the branch
		ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get branch reference: %w", err)
		}
		defer func() { _ = resp.Body.Close() }()

		// Get the commit object that the branch points to
		baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get base commit",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil, nil
		}

		// Create a tree entry for the file deletion by setting SHA to nil
		treeEntries := []*github.TreeEntry{
			{
				Path: github.Ptr(path),
				Mode: github.Ptr("100644"), // Regular file mode
				Type: github.Ptr("blob"),
				SHA:  nil, // Setting SHA to nil deletes the file
			},
		}

		// Create a new tree with the deletion
		newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, treeEntries)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create tree",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusCreated {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to create tree: %s", string(body))), nil, nil
		}

		// Create a new commit with the new tree
		commit := github.Commit{
			Message: github.Ptr(message),
			Tree:    newTree,
			Parents: []*github.Commit{{SHA: baseCommit.SHA}},
		}
		newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create commit",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusCreated {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to create commit: %s", string(body))), nil, nil
		}

		// Update the branch reference to point to the new commit
		ref.Object.SHA = newCommit.SHA
		_, resp, err = client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
			SHA:   *newCommit.SHA,
			Force: github.Ptr(false),
		})
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to update reference",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to update reference: %s", string(body))), nil, nil
		}

		// Create a response similar to what the DeleteFile API would return
		response := map[string]interface{}{
			"commit":  newCommit,
			"content": nil,
		}

		r, err := json.Marshal(response)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// CreateBranch creates a tool to create a new branch.
func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "create_branch",
		Description: t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_CREATE_BRANCH_USER_TITLE", "Create branch"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"branch": {
					Type:        "string",
					Description: "Name for new branch",
				},
				"from_branch": {
					Type:        "string",
					Description: "Source branch (defaults to repo default)",
				},
			},
			Required: []string{"owner", "repo", "branch"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		branch, err := RequiredParam[string](args, "branch")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		fromBranch, err := OptionalParam[string](args, "from_branch")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		// Get the source branch SHA
		var ref *github.Reference

		if fromBranch == "" {
			// Get default branch if from_branch not specified
			repository, resp, err := client.Repositories.Get(ctx, owner, repo)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to get repository",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			fromBranch = *repository.DefaultBranch
		}

		// Get SHA of source branch
		ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+fromBranch)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get reference",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		// Create new branch
		newRef := github.CreateRef{
			Ref: "refs/heads/" + branch,
			SHA: *ref.Object.SHA,
		}

		createdRef, resp, err := client.Git.CreateRef(ctx, owner, repo, newRef)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create branch",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		r, err := json.Marshal(createdRef)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// PushFiles creates a tool to push multiple files in a single commit to a GitHub repository.
func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "push_files",
		Description: t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_PUSH_FILES_USER_TITLE", "Push files to repository"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"branch": {
					Type:        "string",
					Description: "Branch to push to",
				},
				"files": {
					Type:        "array",
					Description: "Array of file objects to push, each object with path (string) and content (string)",
					Items: &jsonschema.Schema{
						Type: "object",
						Properties: map[string]*jsonschema.Schema{
							"path": {
								Type:        "string",
								Description: "path to the file",
							},
							"content": {
								Type:        "string",
								Description: "file content",
							},
						},
						Required: []string{"path", "content"},
					},
				},
				"message": {
					Type:        "string",
					Description: "Commit message",
				},
			},
			Required: []string{"owner", "repo", "branch", "files", "message"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		branch, err := RequiredParam[string](args, "branch")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		message, err := RequiredParam[string](args, "message")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		// Parse files parameter - this should be an array of objects with path and content
		filesObj, ok := args["files"].([]interface{})
		if !ok {
			return utils.NewToolResultError("files parameter must be an array of objects with path and content"), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		// Get the reference for the branch
		ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get branch reference",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		// Get the commit object that the branch points to
		baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get base commit",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		// Create tree entries for all files
		var entries []*github.TreeEntry

		for _, file := range filesObj {
			fileMap, ok := file.(map[string]interface{})
			if !ok {
				return utils.NewToolResultError("each file must be an object with path and content"), nil, nil
			}

			path, ok := fileMap["path"].(string)
			if !ok || path == "" {
				return utils.NewToolResultError("each file must have a path"), nil, nil
			}

			content, ok := fileMap["content"].(string)
			if !ok {
				return utils.NewToolResultError("each file must have content"), nil, nil
			}

			// Create a tree entry for the file
			entries = append(entries, &github.TreeEntry{
				Path:    github.Ptr(path),
				Mode:    github.Ptr("100644"), // Regular file mode
				Type:    github.Ptr("blob"),
				Content: github.Ptr(content),
			})
		}

		// Create a new tree with the file entries
		newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create tree",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		// Create a new commit
		commit := github.Commit{
			Message: github.Ptr(message),
			Tree:    newTree,
			Parents: []*github.Commit{{SHA: baseCommit.SHA}},
		}
		newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to create commit",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		// Update the reference to point to the new commit
		ref.Object.SHA = newCommit.SHA
		updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
			SHA:   *newCommit.SHA,
			Force: github.Ptr(false),
		})
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to update reference",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		r, err := json.Marshal(updatedRef)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// ListTags creates a tool to list tags in a GitHub repository.
func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "list_tags",
		Description: t("TOOL_LIST_TAGS_DESCRIPTION", "List git tags in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_LIST_TAGS_USER_TITLE", "List tags"),
			ReadOnlyHint: true,
		},
		InputSchema: WithPagination(&jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
			},
			Required: []string{"owner", "repo"},
		}),
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		pagination, err := OptionalPaginationParams(args)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		opts := &github.ListOptions{
			Page:    pagination.Page,
			PerPage: pagination.PerPage,
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to list tags",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to list tags: %s", string(body))), nil, nil
		}

		r, err := json.Marshal(tags)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// GetTag creates a tool to get details about a specific tag in a GitHub repository.
func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "get_tag",
		Description: t("TOOL_GET_TAG_DESCRIPTION", "Get details about a specific git tag in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_GET_TAG_USER_TITLE", "Get tag details"),
			ReadOnlyHint: true,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"tag": {
					Type:        "string",
					Description: "Tag name",
				},
			},
			Required: []string{"owner", "repo", "tag"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		tag, err := RequiredParam[string](args, "tag")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		// First get the tag reference
		ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get tag reference",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to get tag reference: %s", string(body))), nil, nil
		}

		// Then get the tag object
		tagObj, resp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				"failed to get tag object",
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to get tag object: %s", string(body))), nil, nil
		}

		r, err := json.Marshal(tagObj)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// ListReleases creates a tool to list releases in a GitHub repository.
func ListReleases(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "list_releases",
		Description: t("TOOL_LIST_RELEASES_DESCRIPTION", "List releases in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_LIST_RELEASES_USER_TITLE", "List releases"),
			ReadOnlyHint: true,
		},
		InputSchema: WithPagination(&jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
			},
			Required: []string{"owner", "repo"},
		}),
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		pagination, err := OptionalPaginationParams(args)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		opts := &github.ListOptions{
			Page:    pagination.Page,
			PerPage: pagination.PerPage,
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		releases, resp, err := client.Repositories.ListReleases(ctx, owner, repo, opts)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to list releases: %w", err)
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to list releases: %s", string(body))), nil, nil
		}

		r, err := json.Marshal(releases)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// GetLatestRelease creates a tool to get the latest release in a GitHub repository.
func GetLatestRelease(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "get_latest_release",
		Description: t("TOOL_GET_LATEST_RELEASE_DESCRIPTION", "Get the latest release in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_GET_LATEST_RELEASE_USER_TITLE", "Get latest release"),
			ReadOnlyHint: true,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
			},
			Required: []string{"owner", "repo"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		release, resp, err := client.Repositories.GetLatestRelease(ctx, owner, repo)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get latest release: %w", err)
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to get latest release: %s", string(body))), nil, nil
		}

		r, err := json.Marshal(release)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

func GetReleaseByTag(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "get_release_by_tag",
		Description: t("TOOL_GET_RELEASE_BY_TAG_DESCRIPTION", "Get a specific release by its tag name in a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_GET_RELEASE_BY_TAG_USER_TITLE", "Get a release by tag name"),
			ReadOnlyHint: true,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
				"tag": {
					Type:        "string",
					Description: "Tag name (e.g., 'v1.0.0')",
				},
			},
			Required: []string{"owner", "repo", "tag"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		tag, err := RequiredParam[string](args, "tag")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		release, resp, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				fmt.Sprintf("failed to get release by tag: %s", tag),
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != http.StatusOK {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to get release by tag: %s", string(body))), nil, nil
		}

		r, err := json.Marshal(release)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// filterPaths filters the entries in a GitHub tree to find paths that
// match the given suffix.
// maxResults limits the number of results returned to first maxResults entries,
// a maxResults of -1 means no limit.
// It returns a slice of strings containing the matching paths.
// Directories are returned with a trailing slash.
func filterPaths(entries []*github.TreeEntry, path string, maxResults int) []string {
	// Remove trailing slash for matching purposes, but flag whether we
	// only want directories.
	dirOnly := false
	if strings.HasSuffix(path, "/") {
		dirOnly = true
		path = strings.TrimSuffix(path, "/")
	}

	matchedPaths := []string{}
	for _, entry := range entries {
		if len(matchedPaths) == maxResults {
			break // Limit the number of results to maxResults
		}
		if dirOnly && entry.GetType() != "tree" {
			continue // Skip non-directory entries if dirOnly is true
		}
		entryPath := entry.GetPath()
		if entryPath == "" {
			continue // Skip empty paths
		}
		if strings.HasSuffix(entryPath, path) {
			if entry.GetType() == "tree" {
				entryPath += "/" // Return directories with a trailing slash
			}
			matchedPaths = append(matchedPaths, entryPath)
		}
	}
	return matchedPaths
}

// resolveGitReference takes a user-provided ref and sha and resolves them into a
// definitive commit SHA and its corresponding fully-qualified reference.
//
// The resolution logic follows a clear priority:
//
//  1. If a specific commit `sha` is provided, it takes precedence and is used directly,
//     and all reference resolution is skipped.
//
//  2. If no `sha` is provided, the function resolves the `ref`
//     string into a fully-qualified format (e.g., "refs/heads/main") by trying
//     the following steps in order:
//     a). **Empty Ref:** If `ref` is empty, the repository's default branch is used.
//     b). **Fully-Qualified:** If `ref` already starts with "refs/", it's considered fully
//     qualified and used as-is.
//     c). **Partially-Qualified:** If `ref` starts with "heads/" or "tags/", it is
//     prefixed with "refs/" to make it fully-qualified.
//     d). **Short Name:** Otherwise, the `ref` is treated as a short name. The function
//     first attempts to resolve it as a branch ("refs/heads/<ref>"). If that
//     returns a 404 Not Found error, it then attempts to resolve it as a tag
//     ("refs/tags/<ref>").
//
//  3. **Final Lookup:** Once a fully-qualified ref is determined, a final API call
//     is made to fetch that reference's definitive commit SHA.
//
// Any unexpected (non-404) errors during the resolution process are returned
// immediately. All API errors are logged with rich context to aid diagnostics.
func resolveGitReference(ctx context.Context, githubClient *github.Client, owner, repo, ref, sha string) (*raw.ContentOpts, error) {
	// 1) If SHA explicitly provided, it's the highest priority.
	if sha != "" {
		return &raw.ContentOpts{Ref: "", SHA: sha}, nil
	}

	originalRef := ref // Keep original ref for clearer error messages down the line.

	// 2) If no SHA is provided, we try to resolve the ref into a fully-qualified format.
	var reference *github.Reference
	var resp *github.Response
	var err error

	switch {
	case originalRef == "":
		// 2a) If ref is empty, determine the default branch.
		repoInfo, resp, err := githubClient.Repositories.Get(ctx, owner, repo)
		if err != nil {
			_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get repository info", resp, err)
			return nil, fmt.Errorf("failed to get repository info: %w", err)
		}
		ref = fmt.Sprintf("refs/heads/%s", repoInfo.GetDefaultBranch())
	case strings.HasPrefix(originalRef, "refs/"):
		// 2b) Already fully qualified. The reference will be fetched at the end.
	case strings.HasPrefix(originalRef, "heads/") || strings.HasPrefix(originalRef, "tags/"):
		// 2c) Partially qualified. Make it fully qualified.
		ref = "refs/" + originalRef
	default:
		// 2d) It's a short name, so we try to resolve it to either a branch or a tag.
		branchRef := "refs/heads/" + originalRef
		reference, resp, err = githubClient.Git.GetRef(ctx, owner, repo, branchRef)

		if err == nil {
			ref = branchRef // It's a branch.
		} else {
			// The branch lookup failed. Check if it was a 404 Not Found error.
			ghErr, isGhErr := err.(*github.ErrorResponse)
			if isGhErr && ghErr.Response.StatusCode == http.StatusNotFound {
				tagRef := "refs/tags/" + originalRef
				reference, resp, err = githubClient.Git.GetRef(ctx, owner, repo, tagRef)
				if err == nil {
					ref = tagRef // It's a tag.
				} else {
					// The tag lookup also failed. Check if it was a 404 Not Found error.
					ghErr2, isGhErr2 := err.(*github.ErrorResponse)
					if isGhErr2 && ghErr2.Response.StatusCode == http.StatusNotFound {
						return nil, fmt.Errorf("could not resolve ref %q as a branch or a tag", originalRef)
					}
					// The tag lookup failed for a different reason.
					_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get reference (tag)", resp, err)
					return nil, fmt.Errorf("failed to get reference for tag '%s': %w", originalRef, err)
				}
			} else {
				// The branch lookup failed for a different reason.
				_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get reference (branch)", resp, err)
				return nil, fmt.Errorf("failed to get reference for branch '%s': %w", originalRef, err)
			}
		}
	}

	if reference == nil {
		reference, resp, err = githubClient.Git.GetRef(ctx, owner, repo, ref)
		if err != nil {
			_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get final reference", resp, err)
			return nil, fmt.Errorf("failed to get final reference for %q: %w", ref, err)
		}
	}

	sha = reference.GetObject().GetSHA()
	return &raw.ContentOpts{Ref: ref, SHA: sha}, nil
}

// ListStarredRepositories creates a tool to list starred repositories for the authenticated user or a specified user.
func ListStarredRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "list_starred_repositories",
		Description: t("TOOL_LIST_STARRED_REPOSITORIES_DESCRIPTION", "List starred repositories"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_LIST_STARRED_REPOSITORIES_USER_TITLE", "List starred repositories"),
			ReadOnlyHint: true,
		},
		InputSchema: WithPagination(&jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"username": {
					Type:        "string",
					Description: "Username to list starred repositories for. Defaults to the authenticated user.",
				},
				"sort": {
					Type:        "string",
					Description: "How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to).",
					Enum:        []any{"created", "updated"},
				},
				"direction": {
					Type:        "string",
					Description: "The direction to sort the results by.",
					Enum:        []any{"asc", "desc"},
				},
			},
		}),
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		username, err := OptionalParam[string](args, "username")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		sort, err := OptionalParam[string](args, "sort")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		direction, err := OptionalParam[string](args, "direction")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		pagination, err := OptionalPaginationParams(args)
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		opts := &github.ActivityListStarredOptions{
			ListOptions: github.ListOptions{
				Page:    pagination.Page,
				PerPage: pagination.PerPage,
			},
		}
		if sort != "" {
			opts.Sort = sort
		}
		if direction != "" {
			opts.Direction = direction
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		var repos []*github.StarredRepository
		var resp *github.Response
		if username == "" {
			// List starred repositories for the authenticated user
			repos, resp, err = client.Activity.ListStarred(ctx, "", opts)
		} else {
			// List starred repositories for a specific user
			repos, resp, err = client.Activity.ListStarred(ctx, username, opts)
		}

		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				fmt.Sprintf("failed to list starred repositories for user '%s'", username),
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != 200 {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to list starred repositories: %s", string(body))), nil, nil
		}

		// Convert to minimal format
		minimalRepos := make([]MinimalRepository, 0, len(repos))
		for _, starredRepo := range repos {
			repo := starredRepo.Repository
			minimalRepo := MinimalRepository{
				ID:            repo.GetID(),
				Name:          repo.GetName(),
				FullName:      repo.GetFullName(),
				Description:   repo.GetDescription(),
				HTMLURL:       repo.GetHTMLURL(),
				Language:      repo.GetLanguage(),
				Stars:         repo.GetStargazersCount(),
				Forks:         repo.GetForksCount(),
				OpenIssues:    repo.GetOpenIssuesCount(),
				Private:       repo.GetPrivate(),
				Fork:          repo.GetFork(),
				Archived:      repo.GetArchived(),
				DefaultBranch: repo.GetDefaultBranch(),
			}

			if repo.UpdatedAt != nil {
				minimalRepo.UpdatedAt = repo.UpdatedAt.Format("2006-01-02T15:04:05Z")
			}

			minimalRepos = append(minimalRepos, minimalRepo)
		}

		r, err := json.Marshal(minimalRepos)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to marshal starred repositories: %w", err)
		}

		return utils.NewToolResultText(string(r)), nil, nil
	})

	return tool, handler
}

// StarRepository creates a tool to star a repository.
func StarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "star_repository",
		Description: t("TOOL_STAR_REPOSITORY_DESCRIPTION", "Star a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_STAR_REPOSITORY_USER_TITLE", "Star repository"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
			},
			Required: []string{"owner", "repo"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		resp, err := client.Activity.Star(ctx, owner, repo)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				fmt.Sprintf("failed to star repository %s/%s", owner, repo),
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != 204 {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to star repository: %s", string(body))), nil, nil
		}

		return utils.NewToolResultText(fmt.Sprintf("Successfully starred repository %s/%s", owner, repo)), nil, nil
	})

	return tool, handler
}

// UnstarRepository creates a tool to unstar a repository.
func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	tool := mcp.Tool{
		Name:        "unstar_repository",
		Description: t("TOOL_UNSTAR_REPOSITORY_DESCRIPTION", "Unstar a GitHub repository"),
		Annotations: &mcp.ToolAnnotations{
			Title:        t("TOOL_UNSTAR_REPOSITORY_USER_TITLE", "Unstar repository"),
			ReadOnlyHint: false,
		},
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"owner": {
					Type:        "string",
					Description: "Repository owner",
				},
				"repo": {
					Type:        "string",
					Description: "Repository name",
				},
			},
			Required: []string{"owner", "repo"},
		},
	}

	handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
		owner, err := RequiredParam[string](args, "owner")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}
		repo, err := RequiredParam[string](args, "repo")
		if err != nil {
			return utils.NewToolResultError(err.Error()), nil, nil
		}

		client, err := getClient(ctx)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
		}

		resp, err := client.Activity.Unstar(ctx, owner, repo)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx,
				fmt.Sprintf("failed to unstar repository %s/%s", owner, repo),
				resp,
				err,
			), nil, nil
		}
		defer func() { _ = resp.Body.Close() }()

		if resp.StatusCode != 204 {
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read response body: %w", err)
			}
			return utils.NewToolResultError(fmt.Sprintf("failed to unstar repository: %s", string(body))), nil, nil
		}

		return utils.NewToolResultText(fmt.Sprintf("Successfully unstarred repository %s/%s", owner, repo)), nil, nil
	})

	return tool, handler
}
